﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Kocic.RepositoryManagement
{
    /// <summary>
    /// Concrete base class implementation for all data managers that are to have Create, Update, and Delete functionality.
    /// </summary>
    /// <typeparam name="TDataObject">The type of the data object that data manager operations will be implemented for.</typeparam>
    /// <typeparam name="TDataSource">The type of the data source that will be used for all operations.</typeparam>
    public abstract class DataManager<TDataObject, TDataSource> : DataSourceManager<TDataSource>
        where TDataObject : class
        where TDataSource : class
    {
        /// <summary>
        /// Creates an instance of the DataManager class.
        /// </summary>
        protected DataManager(TDataSource dataSource)
        {
            if (dataSource == null)
                throw new ArgumentNullException("dataSource", "The data source is null.");

            this.DataSource = dataSource;

            /* This value is set to by default from this constructor. This is to do denote whether or not to dispose the object context after usage or not to in case
             * another manager may be re-using the context*/
            this.DisposeDataSource = true;
        }

        /// <summary>
        /// Specifies the save method that was used to persist data to the underlying data source.
        /// </summary>
        private SaveMethod _method = SaveMethod.None;

        /// <summary>
        /// Handles specific data object events.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The event arguments.</param>
        public delegate void EventHandler(Object sender, DataObjectEventArgs<TDataObject> e);

        /// <summary>
        /// Occurs after a data object has been created.
        /// </summary>
        public event EventHandler<DataObjectEventArgs<TDataObject>> Created;

        /// <summary>
        /// Occurs after a data object has been updated.
        /// </summary>
        public event EventHandler<DataObjectEventArgs<TDataObject>> Updated;

        /// <summary>
        /// Occurs after a data object has been deleted.
        /// </summary>
        public event EventHandler<DataObjectEventArgs<TDataObject>> Deleted;

        /// <summary>
        /// Occurs before a data object has been created.
        /// </summary>
        public event EventHandler<DataObjectEventArgs<TDataObject>> Creating;

        /// <summary>
        /// Occurs before a data object has been updated.
        /// </summary>
        public event EventHandler<DataObjectEventArgs<TDataObject>> Updating;

        /// <summary>
        /// Occurs before a data object has been deleted.
        /// </summary>
        public event EventHandler<DataObjectEventArgs<TDataObject>> Deleting;

        /// <summary>
        /// Raises the Created event.
        /// </summary>
        /// <param name="e">Data object event arguments.</param>
        protected virtual void OnDataObjectCreated(DataObjectEventArgs<TDataObject> e)
        {
            EventHandler<DataObjectEventArgs<TDataObject>> handler = this.Created;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the Updated event.
        /// </summary>
        /// <param name="e">Data object event arguments.</param>
        protected virtual void OnDataObjectUpdated(DataObjectEventArgs<TDataObject> e)
        {
            EventHandler<DataObjectEventArgs<TDataObject>> handler = this.Updated;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the Deleted event.
        /// </summary>
        /// <param name="e">Data object event arguments.</param>
        protected virtual void OnDataObjectDeleted(DataObjectEventArgs<TDataObject> e)
        {
            EventHandler<DataObjectEventArgs<TDataObject>> handler = this.Deleted;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the Creating event.
        /// </summary>
        /// <param name="e">Data object event arguments.</param>
        protected virtual void OnDataObjectCreating(DataObjectEventArgs<TDataObject> e)
        {
            EventHandler<DataObjectEventArgs<TDataObject>> handler = this.Creating;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the Updating event.
        /// </summary>
        /// <param name="e">Data object event arguments.</param>
        protected virtual void OnDataObjectUpdating(DataObjectEventArgs<TDataObject> e)
        {
            EventHandler<DataObjectEventArgs<TDataObject>> handler = this.Updating;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the Deleting event.
        /// </summary>
        /// <param name="e">Data object event arguments.</param>
        protected virtual void OnDataObjectDeleting(DataObjectEventArgs<TDataObject> e)
        {
            EventHandler<DataObjectEventArgs<TDataObject>> handler = this.Deleting;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Gets the save method that was used to persist data to the underlying data source.
        /// </summary>
        public SaveMethod Method
        {
            get
            {
                return _method;
            }
            protected set
            {
                _method = value;
            }
        }

        /// <summary>
        /// Checks to see if a deferrer instance was created.
        /// </summary>
        /// <returns>Value indicating if the deferrer instance was created.</returns>
        protected virtual bool IsDeferralInstanceInstantiated()
        {
            return false;
        }

        /// <summary>
        /// Executes any deferral operations for the provided data object.
        /// </summary>
        /// <param name="dataObject">The data object to defer operations on.</param>
        protected virtual void DeferrerOperations(TDataObject dataObject)
        {

        }

        /// <summary>
        /// Executes any commit operations on the data object.
        /// </summary>
        /// <param name="dataObject">The data object to commit.</param>
        /// <returns>Boolean value indicating if data was commited or not. If data was not commited, then it was deferred.</returns>
        protected virtual bool DataObjectCommitOperations(TDataObject dataObject)
        {
            if (!this.IsDeferralInstanceInstantiated())
            {
                this.Commit(dataObject);
                return true;
            }
            else
            {
                this.DeferrerOperations(dataObject);
                return false;
            }
        }

        /// <summary>
        /// Defines method for operations to add a data object to the data source.
        /// </summary>
        /// <param name="dataObject">The data object to add.</param>
        protected abstract void DataSourceCreateOperations(TDataObject dataObject);

        /// <summary>
        /// Defines method for operations to update data object in the data source.
        /// </summary>
        /// <param name="dataObject">The data object to update.</param>
        protected abstract void DataSourceUpdateOperations(TDataObject dataObject);

        /// <summary>
        /// Defines method for operations to delete a data object from data source.
        /// </summary>
        /// <param name="dataObject">The data object to delete.</param>
        protected abstract void DataSourceDeleteOperations(TDataObject dataObject);

        /// <summary>
        /// Creates a data object and adds it to the data source.
        /// </summary>
        /// <param name="dataObject">The data object used in the create operation.</param>
        protected virtual void Create(TDataObject dataObject)
        {
            this.CoreOperations(dataObject, this.PreCreate, this.DataSourceCreateOperations, this.OnDataObjectCreating, this.OnDataObjectCreated);
        }

        /// <summary>
        /// Updates a data object in the data source.
        /// </summary>
        /// <param name="dataObject">The data object used in the update operation.</param>
        protected virtual void Update(TDataObject dataObject)
        {
            this.CoreOperations(dataObject, this.PreUpdate, this.DataSourceUpdateOperations, this.OnDataObjectUpdating, this.OnDataObjectUpdated);
        }

        /// <summary>
        /// Deletes a data object from the data source.
        /// </summary>
        /// <param name="dataObject">The data object to remove from the data source.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public virtual void Delete(TDataObject dataObject)
        {
            this.CoreOperations(dataObject, this.PreDelete, this.DataSourceDeleteOperations, this.OnDataObjectDeleting, this.OnDataObjectDeleted);
        }

        /// <summary>
        /// Helper method which is responsible for executing Create, Update, Delete operations in a specified templated manner. This method uses delegates defined in the parameters
        /// to denote which method to execute.
        /// </summary>
        /// <param name="dataObject">The data object for which the operation(s) will be executed.</param>
        /// <param name="PreOperation">A delegate which references a Pre operation (PreCreate, PreUpdate, PreDelete).</param>
        /// <param name="DataSourceOperations">A delegate which references a data source operation method, such as DataSourceDeleteOperations, etc.</param>
        /// <param name="OnMainPreOperationRaiseEvent">A delegate which references a method which is called to raise an event before an operation in a data source, on a data object, has taken place.</param>
        /// <param name="OnMainPostDataRaiseEvent">A delegate which references a method which is called to raise an event after an operation in a data source, on a data object, has taken place.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        protected virtual void CoreOperations(TDataObject dataObject,
                                    Func<TDataObject, TDataObject> PreOperation,
                                    Action<TDataObject> DataSourceOperations,
                                    Action<DataObjectEventArgs<TDataObject>> OnMainPreOperationRaiseEvent,
                                    Action<DataObjectEventArgs<TDataObject>> OnMainPostDataRaiseEvent)
        {
            if (this.IsDisposed)
            {
                throw new ObjectDisposedException(this.GetType().Name, "The current instance of the manager has been disposed.");
            }

            if (dataObject == null)
            {
                throw new ArgumentNullException("dataObject", "Data object is null.");
            }

            DataObjectEventArgs<TDataObject> args;

            //Executes a pre-method. This can be PreDelete, PreUpdate, PreCreate.
            TDataObject preDataObject = PreOperation(dataObject);

            if (preDataObject != null)
            {
                args = DataObjectEventArgsFactory<TDataObject>.Create(preDataObject);

                //Raises an event before an actual object is saved, deleted, or updated. For example, this may be OnDataObjectDeleting.
                OnMainPreOperationRaiseEvent(args);

                //Calls the data source operations method. This may be DataSourceCreateOperations, or any other one specified.
                DataSourceOperations(preDataObject);

                this.DataObjectCommitOperations(preDataObject);

                //Raises an event after the data object is saved, deleted, or updated. For example, this may be OnDataObjectDeleted.
                OnMainPostDataRaiseEvent(args);
            }
            else
            {
                //Same operations as described above, except with the dataObject since this block is not checking for any PreOperation values returned.
                args = DataObjectEventArgsFactory<TDataObject>.Create(dataObject);

                OnMainPreOperationRaiseEvent(args);

                DataSourceOperations(dataObject);

                //Invoke post operation event only if commit operations were not deferred
                bool isCommited = this.DataObjectCommitOperations(dataObject);

                if (isCommited)
                {
                    OnMainPostDataRaiseEvent(args);
                }
            }
        }

        /// <summary>
        /// Saves data object to data source. Creates new data or updates existing data. The save method needs to be explicitly specified if the state of the data object, in relation to its context, cannot natively be determined.
        /// </summary>
        /// <param name="dataObject">The data object to save to the data source.</param>
        /// <param name="method">The method of saving a data object to the data source.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public virtual void Save(TDataObject dataObject, SaveMethod method = SaveMethod.None)
        {
            if (this.IsDisposed)
            {
                throw new ObjectDisposedException(this.GetType().Name, "The current instance of the manager has been disposed");
            }

            if (dataObject == null)
            {
                throw new ArgumentNullException("dataObject", "Data object is null.");
            }

            DataObjectEventArgs<TDataObject> args = DataObjectEventArgsFactory<TDataObject>.Create(dataObject);

            if (method == SaveMethod.None)
            {
                Boolean? objectStateDetached = this.IsDataObjectInRemovedState(dataObject);

                //If value is null, then the data object does not support state tracking
                if (!objectStateDetached.HasValue)
                {
                    throw new ArgumentException("The save method must be specified since the current data object's state cannot be determined.", "method");
                }
                else
                {
                    //Is the object currently in a detached state?
                    if (objectStateDetached.Value)
                    {
                        this.Method = SaveMethod.Create;
                        this.Create(dataObject);
                    }
                    else
                    {
                        this.Method = SaveMethod.Update;
                        this.Update(dataObject);
                    }
                }
            }
            else if (method == SaveMethod.Create)
            {
                this.Method = SaveMethod.Create;
                this.Create(dataObject);
            }
            else
            {
                this.Method = SaveMethod.Update;
                this.Update(dataObject);
            }
        }

        /// <summary>
        /// Defines commit method that is used to commit changes to the data source.
        /// </summary>
        /// <param name="dataObject">The data object that is used to commit changes with.</param>
        protected abstract void Commit(TDataObject dataObject = null);

        /// <summary>
        /// Defines how to check the state of an data object.
        /// </summary>
        /// <param name="dataObject">The data object to check the state.</param>
        /// <returns>Value indiciating if the entity object is in a removed state. This can be in a native Detached state or if it's not part of the current context's state manager.</returns>
        protected virtual Boolean? IsDataObjectInRemovedState(TDataObject dataObject)
        {
            return null;
        }

        /// <summary>
        /// Defines how to perform any additional operations with the data object before it is created and added to the data source.
        /// </summary>
        /// <param name="dataObject">The data object used in performing additional operations to prior to adding and saving the changes to the data source.</param>
        protected abstract TDataObject PreCreate(TDataObject dataObject);

        /// <summary>
        /// Defines how to perform any additional operations with the data object before it is updated and its changes saved to the data source.
        /// </summary>
        /// <param name="dataObject">The data object used in performing additional operations to prior to saving its changes to the data source.</param>
        protected abstract TDataObject PreUpdate(TDataObject dataObject);

        /// <summary>
        /// Defines how to perform any additional operations with the data object before it is deleted and removed from the data source.
        /// </summary>
        /// <param name="dataObject">The data object used in performing additional operations to prior to deleting it from the data source and saving the changes.</param>
        protected abstract TDataObject PreDelete(TDataObject dataObject);
    }
}
